package fr.gla.hibtracker.historique.listener;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.criterion.Restrictions;
import org.hibernate.engine.EntityEntry;
import org.hibernate.event.FlushEntityEvent;
import org.hibernate.event.def.DefaultFlushEntityEventListener;
import org.hibernate.impl.CriteriaImpl;
import org.hibernate.persister.entity.EntityPersister;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import fr.gla.hibtracker.historique.beans.ELProperty;
import fr.gla.hibtracker.historique.bm.ConfigModification;
import fr.gla.hibtracker.historique.bm.ConfigModificationID;
import fr.gla.hibtracker.historique.bm.FlushModifications;
import fr.gla.hibtracker.historique.bm.GroupeModifications;
import fr.gla.hibtracker.historique.bm.Modification;
import fr.gla.hibtracker.historique.bm.ModificationID;
import fr.gla.hibtracker.historique.bs.UserResolver;
import fr.gla.hibtracker.historique.listener.EventListenerHelper.ElPropertyInitiatorHandler;

/**
 * <p>
 * Obervateur sur l'évenement flush d'une SessionFactory Hibernate permettant d'enregistrer les modifications ayant eu
 * lieu sur les objets.
 * </p>
 * <p>
 * Il faut spécifier une liste de classes pour lesquelles on désire historiser les modifications. On peut le faire via
 * spring : <code>
 * <pre>
 * &lt;entry key="flush-entity"&gt;
 *     &lt;list&gt;
 *        &lt;bean class="de.historique.listener.DirtyUpdateListener"&gt;
 *            &lt;property name="classNames"&gt;
 *                &lt;set&gt;
 *                    &lt;value>de.bsweb.domain.Affaire&lt;/value&gt;
 *                &lt;/set&gt;
 *            &lt;/property&gt;
 *        &lt;/bean&gt;
 *     &lt;/list&gt;
 * &lt;/entry&gt;
 * </pre>
 * </code> Ou en créant une classe fille : <code>
 * <pre>
 * public class MyDirtyUpdateListener extends DirtyUpdateListener {
 *
 *    public TestDirtyUpdateListener() {
 *        Set<String> classes = new HashSet<String>();
 *        classes.add(Ticket.class.getName());
 *        setClassNames(classes);
 *    }
 * }
 * </pre>
 * </code> qui déclarée comme listener dans le <code>hibernate.cfg.xml</code> : <code>
 * <pre>
 * &lt;listener type="flush-entity" class="de.support.listen.MyDirtyUpdateListener" /&gt;
 * </pre>
 * </code>
 * </p>
 * <p>
 * Il faut également ajouter dans la table <code>HISTO_CONF_PROPERTY</code> correspondant à l'entité
 * {@link ConfigModification} les champs pour lesquels on désire une historisation. Seuls les champs pour lesquels il y
 * a eu configuration sont historisés.
 * </p>
 * <p>
 * <code>DirtyUpdateListener</code> gère un cache des {@link ConfigModification} dont on peut régler l'intervalle de
 * rafraichissement.
 * </p>
 *
 * @see FlushModifications
 */
public class DirtyUpdateListener extends DefaultFlushEntityEventListener {

    private static final long                             serialVersionUID                   = 1L;

    private static final Logger                           LOGGER                           	 = LoggerFactory.getLogger(DirtyUpdateListener.class);

    private Set<String>                                   classNames;

    private long                                          configModificationCacheInterval    = 1800000;

    private Map<String, Class<?>>                         classCaches                        = new HashMap<String, Class<?>>();

    private Map<Class<?>, Class<? extends GroupeModifications<?>>> configGroupeModifications = new HashMap<Class<?>, Class<? extends GroupeModifications<?>>>();
    
    private Map<ConfigModificationID, ConfigModification> configModificationCaches           = new HashMap<ConfigModificationID, ConfigModification>();

    private long                                          timeStampConfigModificationCache   = 0;

    private UserResolver                                  userResolver;

    public DirtyUpdateListener() {
        super();
        LOGGER.trace("Création d'un nouveau {}", getClass().getName());
    }


    /**
     * <p>
     * Permet d'instancier le {@link GroupeModifications} associé à une entité persistante.
     * </p>
     * <p>
     * Par défaut crée simplement une nouvelle instance de {@link GroupeModifications} : <code>
     * <pre>
     * return new GroupeModifications<T>();
     * </pre>
     * </code>
     * </p>
     * <p>
     * Il s'agit d'une méthode crochet : comme le mapping de <code>entity</code> peut être complexe (par exemple avoir
     * un identifiant composite) on permet à la classe fille d'instancier une classe héritant de
     * {@link GroupeModifications} correspondant à la classe qu'il veut historiser et surtout de configurer le mapping
     * associé.
     * </p>
     *
     * @param <T> Type de l'entitée sur laquelle porte {@link GroupeModifications}.
     * @param entity Entitée pour laquelle on instancie {@link GroupeModifications}.
     * @return Renvoie l'instance de {@link GroupeModifications} associée à l'entitée <code>entity</code>.
     */
    protected <T extends Object> GroupeModifications<T> instantiateGroupeModifications(T entity,
                                                                                       FlushEntityEvent entityEvent) {
    	for(Class<?> key : configGroupeModifications.keySet()){
    		if(key.isAssignableFrom(entity.getClass())){
    			try {
					return (GroupeModifications<T>) configGroupeModifications.get(key).newInstance();
				} catch (InstantiationException e) {
					throw new HibernateException("");
				} catch (IllegalAccessException e) {
					throw new HibernateException("");
				}
    		}
    	}
    	
        return new GroupeModifications<T>();
    }


    /**
     * @see org.hibernate.event.def.DefaultFlushEntityEventListener#onFlushEntity(org.hibernate.event.FlushEntityEvent)
     */
    @Override
    public void onFlushEntity(FlushEntityEvent event) throws HibernateException {
        LOGGER.trace("On flush entity {} ({})", event.getEntity(), event.getEntity().getClass());

        // on conserve le comportement implémenté par DefaultFlushEntityEventListener
        super.onFlushEntity(event);

        cacheTreatment(event);

        FlushModifications flushModifications = FlushModificationThreadLocal.getFlushModifications();

        // si on a une entitée non nulle dont la classe correspond à une des classes à traiter
        Object entity = event.getEntity();
        if (entity != null && isCheckable(entity.getClass())) {
            LOGGER.debug("Flush sur l'entité : {}", entity);

            int[] dirtyProperties = event.getDirtyProperties();
            final Object[] values = event.getPropertyValues();
            final EntityEntry entry = event.getEntityEntry();
            final Object[] loadedState = entry.getLoadedState();
            final EntityPersister persister = entry.getPersister();
            final String[] propertyNames = persister.getPropertyNames();
            List<ELProperty> dirtyElProperties = new ArrayList<ELProperty>();

            // si on a des propriétés modifiées.
            if (dirtyProperties != null) {
                LOGGER.debug("Des propriétés ont été modifiées.");
                // pour chacunne
                for (int i = 0; i < dirtyProperties.length; i++) {
                    int dirtyIndex = dirtyProperties[i];
                    String propertyName = propertyNames[dirtyIndex];
                    Object previousValue = (loadedState == null) ? null : loadedState[dirtyIndex];
                    Object newValue = values[dirtyIndex];
                    String el = propertyName;

                    LOGGER.trace("La proprieté {} a été modifiée.", el);

                    // on appelle une méthode récurvive créant les instances du domain model
                    // d'historique des modifications
                    EventListenerHelper.checkDirtyProperties(event,
                                                             previousValue,
                                                             newValue,
                                                             persister.getPropertyType(el),
                                                             ELProperty.class,
                                                             dirtyElProperties,
                                                             el);
                }
            }
            LOGGER.debug("Ajout des modifications pour {} : {}", entity, dirtyElProperties);
            addModificationHistorique(entity, dirtyElProperties, event, flushModifications);

            if (flushModifications != null) {
                Set<GroupeModifications<?>> groupesModifications = flushModifications.getGroupesModifications();
                boolean historiqueModificationsNonVide = !groupesModifications.isEmpty();
                if (historiqueModificationsNonVide) {
                    LOGGER.debug("Sauvegarde des modifications \n {}", flushModifications);

                    // sauvegarde de l'historique des modifs
                    event.getSession().saveOrUpdate(flushModifications);

                    for (GroupeModifications<?> modifications : groupesModifications) {
                        event.getSession().saveOrUpdate(modifications);
                    }
                }
            }
        } else {
            LOGGER.info("Pas de modifications dans le Thread local");
        }
        LOGGER.trace("Fin de la gestion de flush sur {}", entity);
    }

    /**
     * Redefinie l'interception de la sessionFactory
     */
    @Override
    protected boolean handleInterception(FlushEntityEvent event) {
        boolean handleInterception = false;
        Object entity = event.getEntity();
        final Object[] values = event.getPropertyValues();
        final Object[] loadedValues = event.getEntityEntry().getLoadedState();
        final int[] dirtyValues = event.getDirtyProperties();
        String[] propertyNames = event.getEntityEntry().getPersister().getPropertyNames();
        final EntityPersister persister = event.getEntityEntry().getPersister();
        Map<String, ELProperty> propertiesValue = new HashMap<String, ELProperty>();
        Map<String, ELProperty> dirtyPropertiesValue = new HashMap<String, ELProperty>();

        if (dirtyValues != null) {
            for (int i = 0; i < propertyNames.length; i++) {
                Object previousValue = null;
                if (loadedValues != null) {
                    previousValue = loadedValues[i];
                }
                Object newValue = null;
                if (values != null) {
                    newValue = values[i];
                }
                ELProperty value = new ELProperty(propertyNames[i], previousValue, newValue);
                propertiesValue.put(propertyNames[i], value);
            }
            List<ELProperty> elProperties = new ArrayList<ELProperty>();
            ElPropertyInitiatorHandler<ELProperty> handler = new ElPropertyInitiatorHandler<ELProperty>() {
                public void initiate(FlushEntityEvent evt,
                                     ELProperty elProperty,
                                     String el,
                                     Object previousValue,
                                     Object newValue) {
                    elProperty.setDirty(true);
                }
            };
            for (int i = 0; i < dirtyValues.length; i++) {
                int dirtyIndex = dirtyValues[i];
                String propertyName = propertyNames[dirtyIndex];
                assert values != null;
                Object newValue = values[dirtyIndex];
                Object previousValue = (loadedValues == null) ? null : loadedValues[dirtyIndex];
                EventListenerHelper.checkDirtyProperties(event,
                                                         previousValue,
                                                         newValue,
                                                         persister.getPropertyType(propertyName),
                                                         ELProperty.class,
                                                         handler,
                                                         elProperties,
                                                         propertyName);
            }
            dirtyPropertiesValue = EventListenerHelper.transform(elProperties);
            if (customActionOnDirtyProperty(entity, propertiesValue, dirtyPropertiesValue)) {
                handleInterception |= true;
                for (Entry<String, ELProperty> entry : propertiesValue.entrySet()) {
                    int index = Arrays.asList(propertyNames).indexOf(entry.getKey());
                    assert values != null;
                    values[index] = entry.getValue().getNewValue();
                }
                int[] dirtyProperties;
                if (event.hasDatabaseSnapshot()) {
                    dirtyProperties = event.getEntityEntry().getPersister().findModified(event.getDatabaseSnapshot(),
                                                                                         values,
                                                                                         entity,
                                                                                         event.getSession());
                } else {
                    dirtyProperties = event.getEntityEntry().getPersister().findDirty(values,
                                                                                      event.getEntityEntry()
                                                                                           .getLoadedState(),
                                                                                      entity,
                                                                                      event.getSession());
                }
                event.setPropertyValues(values);
                event.setDirtyProperties(dirtyProperties);
            }
        }
        handleInterception |= super.handleInterception(event);
        return handleInterception;
    }

    protected boolean customActionOnDirtyProperty(Object entity,
                                                  Map<String, ELProperty> propertyValues,
                                                  Map<String, ELProperty> dirtyPropertyValues) {
        return false;
    }

    /**
     * Historise les propriétés dirty de l'entité
     */
    @SuppressWarnings("unchecked")
    private void addModificationHistorique(Object entity,
                                           List<ELProperty> dirtyElProperties,
                                           FlushEntityEvent event,
                                           FlushModifications flushModifications) {
        GroupeModifications groupeModifications = instantiateGroupeModifications(entity, event);
        groupeModifications.setObject(entity);
        List<ELProperty> modificationCheckables = new ArrayList<ELProperty>();
        for (ELProperty dirtyELProperty : dirtyElProperties) {
            ConfigModificationID configModificationID = new ConfigModificationID();
            configModificationID.setClassName(entity.getClass().getName());
            configModificationID.setEl(dirtyELProperty.getElName());
            ConfigModification configModification = configModificationCaches.get(configModificationID);
            LOGGER.trace("Recherche de la propriété de configuration {}", configModificationID);
            if (configModification != null) {
                Modification modification = new Modification();
                ModificationID modificationID = new ModificationID();
                modificationID.setConfigModification(configModification);
                modificationID.setGroupeModifications(groupeModifications);
                modification.setOldValue(toStringValue(dirtyELProperty.getPreviousValue()));
                modification.setNewValue(toStringValue(dirtyELProperty.getNewValue()));
                modification.setModificationID(modificationID);
                modificationCheckables.add(dirtyELProperty);

                LOGGER.trace("Ajout de le modification {} au groupe  {}", modification, groupeModifications);
                groupeModifications.getModifications().add(modification);

            } else {
                LOGGER.trace("could not find confProperty for {}", configModificationID);
            }
        }
        LOGGER.trace("Modifications checkables : {}", modificationCheckables);
        if (!modificationCheckables.isEmpty()) {
            GroupeModifications<?> existingGroupeModifications = flushModifications.getGroupeModifications(entity);
            if (existingGroupeModifications == null) {
                LOGGER.trace("Add group modification for object : {}", entity);
                flushModifications.addGroupesModification(groupeModifications);
            } else {
                LOGGER.debug("Group modification already exist for object : {}  add modification properties on", entity);
                existingGroupeModifications.addAllModification(groupeModifications.getModifications());
            }

            FlushModificationThreadLocal.setFlushModifications(flushModifications);
        }
    }


    /**
     * Récupère les configurations de modification et met a jour le cache toutes les configModificationCacheInterval ms
     */
    @SuppressWarnings("unchecked")
    private void cacheTreatment(FlushEntityEvent event) {
        long currentTime = System.currentTimeMillis();
        LOGGER.trace("Current time : {}, 'timeStampConfigModificationCache' : {}",
                     currentTime,
                     timeStampConfigModificationCache);
        if (timeStampConfigModificationCache == 0
                        || (currentTime - timeStampConfigModificationCache) >= getConfigModificationCacheInterval()) {
            timeStampConfigModificationCache = currentTime;
            LOGGER.info("Mise a jour du cache de configuration de modification");

            CriteriaImpl criteria = new CriteriaImpl(ConfigModification.class.getName(), event.getSession());
            criteria.add(Restrictions.eq("isPersist", true));
            List<ConfigModification> configModificationList = event.getSession().list(criteria);
            LOGGER.debug("LIste des 'ConfigModification' : {}", configModificationList);

            for (ConfigModification configModification : configModificationList) {
                LOGGER.trace("Initialisation de {}", configModification);
                Hibernate.initialize(configModification);
                configModificationCaches.put(configModification.getId(), configModification);
            }
        }
    }

    /**
     * Renvoie vrai si la classe <code>clazz</code> fait partie des classes configurée comme devant être checkée par le
     * listener.
     *
     * Utilise <code>isAssignableFrom</code>.
     *
     * @param clazz Classe de l'entitée sur laquelle l'évement a eu lieu.
     * @return Renvoie vrai si la classe <code>clazz</code> fait partie des classes configurée comme devant être checkée
     *         par le listener.
     */
    @SuppressWarnings("unchecked")
    private boolean isCheckable(Class clazz) {
        boolean isAssignable = false;
        for (String clazzName : classNames) {
            try {
                Class clazzConfig = null;
                if ((clazzConfig = classCaches.get(clazzName)) == null) {
                    clazzConfig = Class.forName(clazzName);
                    classCaches.put(clazzName, clazzConfig);
                }
                if (clazzConfig.isAssignableFrom(clazz)) {
                    return true;
                }
            } catch (ClassNotFoundException e) {
                LOGGER.warn("Class " + clazz.getName() + " does not exist, verify your listerner's configuration", e);
            }
        }
        return isAssignable;
    }

    private String toStringValue(Object value) {
        return (value != null) ? value.toString() : "null";
    }

    /**
     * Permet de configurer les classes pour lesquelles on désire un historique.
     *
     * @param classNames Liste de nom de classe persistantes dont on désire historiser les modifications.
     */
    public void setClassNames(Set<String> classNames) {
        this.classNames = classNames;
    }

    /**
     * Permet de configurer l'intervalle de mise à jour du cache des configuration de modifications.
     *
     * @param configModificationCacheInterval Intervalle de mise à jour du cache des configuration de modifications.
     */
    public void setConfigModificationCacheInterval(long configModificationCacheInterval) {
        this.configModificationCacheInterval = configModificationCacheInterval;
    }

    public long getConfigModificationCacheInterval() {
        return configModificationCacheInterval;
    }


    public void setUserResolver(UserResolver userResolver) {
        this.userResolver = userResolver;
    }
    
    public void setConfigGroupeModifications(
			Map<Class<?>, Class<? extends GroupeModifications<?>>> configGroupeModifications) {
		this.configGroupeModifications = configGroupeModifications;
	}
    
    public Map<Class<?>, Class<? extends GroupeModifications<?>>> getConfigGroupeModifications() {
		return configGroupeModifications;
	}
}
